string(PREPEND QtVersion "Qt")
-if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" OR ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD")
find_package(PkgConfig REQUIRED)
pkg_check_modules(JACK REQUIRED IMPORTED_TARGET jack)
if (weakjack)
src/vs/vsWebSocket.cpp
src/vs/vsPermissions.cpp
src/vs/vs.qrc
+ src/vs/WebSocketTransport.cpp
src/images/images.qrc
src/Analyzer.cpp
src/Monitor.cpp
* MESON_ARGS - arguments to build using meson
* QT_DOWNLOAD_URL - path to qt6 download (optional)
* VST3SDK_DOWNLOAD_URL - path to the VST3 SDK (optional)
+* USE_SYSTEM_LIBSAMPLERATE - dynamically link with libsamplerate
For example:
```
docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
--platform linux/arm/v7 --build-arg BUILD_CONTAINER=debian:buster \
+ --build-arg USE_SYSTEM_LIBSAMPLERATE=1 \
--build-arg MESON_ARGS="-Ddefault_library=static -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false -Dnogui=true -Dcpp_link_args='-no-pie'" \
--build-arg QT_DOWNLOAD_URL=https://files.jacktrip.org/contrib/qt/qt-5.15.13-static-linux-arm32.tar.gz .
```
+- Version: "2.6.0"
+ Date: 2025-04-22
+ Description:
+ - (added) OSC endpoint to get latencies for connected clients
+ - (updated) PLC auto headroom allows higher latency when necessary
+ - (updated) VS Mode allow any two consecutive channels for input
+ - (updated) VS Mode easier audio switching between stereo and mono
+ - (updated) VS Mode latency statistics now include jitter buffer
+ - (updated) VS Mode improvements to audio quality override settings
+ - (updated) VS Mode temporarily disabling feedback detection
+ - (fixed) VS Mode kicked out of sessions due to studio change
+ - (fixed) VS Mode recognizes changes to server host and port
+ - (fixed) VS Mode bugs with reconnecting due to audio changes
+ - (fixed) VS Mode strange error message during startup on Linux
+ - (fixed) VS Mode empty studio list when starting up
+ - (fixed) Ability to build VS Mode using CMake
+ - (fixed) Ability to build aarch64 or armv7 on Alpine Linux
+ - (fixed) Ability to build using Qt 6.9 release candidates
+ - (fixed) Ability to disable the use of libsamplerate
+ - (fixed) Ignore timestamps when generating jacktrip.1.gz
- Version: "2.5.1"
Date: 2025-01-30
Description:
defined(__ia64__) || defined(_M_IX86) || defined(_M_IA64) || \
defined(_M_ALPHA) || defined(__amd64) || defined(__amd64__) || \
defined(_M_AMD64) || defined(__x86_64) || defined(__x86_64__) || \
- defined(_M_X64) || defined(__bfin__)
+ defined(_M_X64) || defined(__bfin__) || defined(__aarch64__) || \
+ defined(__ARM_EABI__)
# define OSCPP_LITTLE_ENDIAN
# define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN
# this Dockerfile is used by GitHub CI to create linux builds
# it requires these environment variables:
#
-# BUILD_CONTAINER - Debian based container image to build with
-# MESON_ARGS - arguments to build using meson
-# QT_DOWNLOAD_URL - path to qt download (optional)
+# BUILD_CONTAINER - Debian based container image to build with
+# MESON_ARGS - arguments to build using meson
+# QT_DOWNLOAD_URL - path to qt download (optional)
+# USE_SYSTEM_LIBSAMPLERATE - dynamically link with libsamplerate
# container image versions
ARG BUILD_CONTAINER=ubuntu:20.04
WORKDIR /opt/jacktrip
+# install libsamplerate
+ARG USE_SYSTEM_LIBSAMPLERATE=""
+ENV USE_SYSTEM_LIBSAMPLERATE=$USE_SYSTEM_LIBSAMPLERATE
+RUN if [ -n "$USE_SYSTEM_LIBSAMPLERATE" ]; then \
+ apt-get install -yq --no-install-recommends libsamplerate-dev; \
+ fi
+
# install qt
ARG QT_DOWNLOAD_URL=""
ENV QT_DOWNLOAD_URL=$QT_DOWNLOAD_URL
+++ /dev/null
-[constants]
-minversion_args = ['-mmacosx-version-min=11']
-
-[built-in options]
-c_args = minversion_args
-cpp_args = minversion_args
-objcpp_args = minversion_args
-c_link_args = minversion_args
-cpp_link_args = minversion_args
-objcpp_link_args = minversion_args
--- /dev/null
+[constants]
+minversion_args = ['-mmacosx-version-min=12']
+
+[built-in options]
+c_args = minversion_args
+cpp_args = minversion_args
+objcpp_args = minversion_args
+c_link_args = minversion_args
+cpp_link_args = minversion_args
+objcpp_link_args = minversion_args
+++ /dev/null
-[constants]
-minversion_args = ['-mmacosx-version-min=11']
-universal_args = ['-arch', 'x86_64', '-arch', 'arm64']
-
-[built-in options]
-c_args = universal_args + minversion_args
-cpp_args = universal_args + minversion_args
-objcpp_args = universal_args + minversion_args
-c_link_args = universal_args + minversion_args
-cpp_link_args = universal_args + minversion_args
-objcpp_link_args = universal_args + minversion_args
--- /dev/null
+[constants]
+minversion_args = ['-mmacosx-version-min=12']
+universal_args = ['-arch', 'x86_64', '-arch', 'arm64']
+
+[built-in options]
+c_args = universal_args + minversion_args
+cpp_args = universal_args + minversion_args
+objcpp_args = universal_args + minversion_args
+c_link_args = universal_args + minversion_args
+cpp_link_args = universal_args + minversion_args
+objcpp_link_args = universal_args + minversion_args
qt_plugindir = run_command(qmake, '-query', 'QT_INSTALL_PLUGINS', check : true).stdout().strip()
if qt_version == '6'
# qt6 requires "Bundled*" modules for linking
- static_deps += dependency('qt6', modules: ['DBus', 'BundledLibpng', 'BundledPcre2', 'BundledHarfbuzz', 'BundledZLIB'], include_type: 'system')
+ static_deps += compiler.find_library('Qt6BundledLibpng', required : true, dirs : [qt_libdir])
+ static_deps += compiler.find_library('Qt6BundledPcre2', required : true, dirs : [qt_libdir])
+ static_deps += compiler.find_library('Qt6BundledHarfbuzz', required : true, dirs : [qt_libdir])
+ zlib_dep = compiler.find_library('Qt6BundledZLIB', required : false, dirs : [qt_libdir])
+ if zlib_dep.found()
+ static_deps += zlib_dep
+ endif
+ dbus_dep = compiler.find_library('Qt6DBus', required : false, dirs : [qt_libdir])
+ if dbus_dep.found()
+ static_deps += dbus_dep
+ endif
else
static_deps += compiler.find_library('qtpcre2', required : true, dirs : [qt_libdir])
endif
static_link_args += ['-framework', 'Security']
static_link_args += ['-framework', 'GSS']
static_link_args += ['-framework', 'SystemConfiguration']
+ static_link_args += ['-framework', 'UniformTypeIdentifiers']
+ static_link_args += '-lresolv'
static_deps += dependency('zlib', required : true)
endif
endif
configure.''')
endif
-libsamplerate_dep = []
found_libsamplerate = false
if get_option('libsamplerate').allowed()
- opt_var = cmake.subproject_options()
- if get_option('buildtype') == 'release'
- opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'})
+ libsamplerate_dep = dependency('samplerate', required: false)
+ if libsamplerate_dep.found()
+ found_libsamplerate = true
else
- opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'})
- endif
- opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
- libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var)
- libsamplerate_dep = libsamplerate_subproject.dependency('samplerate')
- found_libsamplerate = libsamplerate_dep.found()
- if not found_libsamplerate and not get_option('libsamplerate').auto()
- error('failed to configure libsamplerate')
- endif
- if found_libsamplerate
- defines += '-DHAVE_LIBSAMPLERATE'
- deps += libsamplerate_dep
+ opt_var = cmake.subproject_options()
+ if get_option('buildtype') == 'release'
+ opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'})
+ else
+ opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'})
+ endif
+ opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
+ libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var)
+ libsamplerate_dep = libsamplerate_subproject.dependency('samplerate')
+ found_libsamplerate = libsamplerate_dep.found()
+ if not found_libsamplerate and not get_option('libsamplerate').auto()
+ error('failed to configure libsamplerate')
+ endif
endif
endif
+if found_libsamplerate
+ defines += '-DHAVE_LIBSAMPLERATE'
+ deps += libsamplerate_dep
+endif
if host_machine.system() == 'darwin'
src += ['src/NoNap.mm']
custom_target('jacktrip.1.gz',
input: manfile,
output: 'jacktrip.1.gz',
- command: [gzip, '-k', '-f', '@INPUT@'],
+ command: [gzip, '-k', '-f', '-n', '@INPUT@'],
install: true,
install_dir: get_option('mandir') / 'man1')
endif
#include "JackTrip.h"
#include "ProcessPlugin.h"
+#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
+#define STD_AS_CONST qAsConst
+#else
+#define STD_AS_CONST std::as_const
+#endif
+
using std::cout;
using std::endl;
, mMonitorStarted(false)
, mJackTrip(jacktrip)
, mInputMixMode(InputMixMode)
+ , mAudioInputLatency(0)
+ , mAudioOutputLatency(0)
, mProcessingAudio(false)
{
}
}
#ifndef WAIR
- for (auto& s : qAsConst(mAudioSockets)) {
+ for (auto& s : STD_AS_CONST(mAudioSockets)) {
s->getFromAudioSocketPlugin()->compute(n_frames, in_buffer.data(),
in_buffer.data());
}
#endif // not WAIR
// process incoming signal from audio interface using process plugins
- for (auto& p : qAsConst(mProcessPluginsToNetwork)) {
+ for (auto& p : STD_AS_CONST(mProcessPluginsToNetwork)) {
if (p->getInited()) {
p->compute(n_frames, in_buffer.data(), in_buffer.data());
}
/// with one. do it chaining outputs to inputs in the buffers. May need a tempo buffer
#ifndef WAIR // NOT WAIR:
- for (auto& p : qAsConst(mProcessPluginsFromNetwork)) {
+ for (auto& p : STD_AS_CONST(mProcessPluginsFromNetwork)) {
if (p->getInited()) {
p->compute(n_frames, out_buffer.data(), out_buffer.data());
}
}
}
- for (auto& s : qAsConst(mAudioSockets)) {
+ for (auto& s : STD_AS_CONST(mAudioSockets)) {
s->getToAudioSocketPlugin()->compute(n_frames, out_buffer.data(),
out_buffer.data());
}
<< ") at sampling rate " << mSampleRate << "\n";
}
- for (auto& plugin : qAsConst(mProcessPluginsFromNetwork)) {
+ for (auto& plugin : STD_AS_CONST(mProcessPluginsFromNetwork)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (auto& plugin : qAsConst(mProcessPluginsToNetwork)) {
+ for (auto& plugin : STD_AS_CONST(mProcessPluginsToNetwork)) {
plugin->setOutgoingToNetwork(true);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (auto& plugin : qAsConst(mProcessPluginsToMonitor)) {
+ for (auto& plugin : STD_AS_CONST(mProcessPluginsToMonitor)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansMon, nChansMon);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (auto& s : qAsConst(mAudioSockets)) {
+ for (auto& s : STD_AS_CONST(mAudioSockets)) {
auto* plugin = s->getFromAudioSocketPlugin().get();
plugin->setOutgoingToNetwork(true);
plugin->updateNumChannels(nChansIn, nChansOut);
const std::string& getDevicesErrorMsg() const { return mErrorMsg; }
const std::string& getDevicesWarningHelpUrl() const { return mWarningHelpUrl; }
const std::string& getDevicesErrorHelpUrl() const { return mErrorHelpUrl; }
+ double getAudioInputLatency() const { return mAudioInputLatency; }
+ double getAudioOutputLatency() const { return mAudioOutputLatency; }
bool highLatencyBufferSize() const { return getBufferSizeInSamples() > 256; }
bool getHighLatencyFlag() const { return mHighLatencyFlag; }
//------------------------------------------------------------------
protected:
JackTrip* mJackTrip; ///< JackTrip Mediator Class pointer
inputMixModeT mInputMixMode; ///< Input mixing mode
+ double mAudioInputLatency; ///< Latency of the audio input
+ double mAudioOutputLatency; ///< Latency of the audio output
void setDevicesWarningMsg(warningMessageT msg);
void setDevicesErrorMsg(errorMessageT msg);
void AudioSocketWorker::scheduleReconnect()
{
if (mRetryConnection) {
- qDebug() << "Attempting to reconnect audio socket";
+ cout << "Attempting to reconnect audio socket" << endl;
if (mTimerPtr.isNull()) {
mTimerPtr.reset(new QTimer);
QObject::connect(mTimerPtr.data(), &QTimer::timeout, this,
return (mAudioInterface == nullptr) ? false
: mAudioInterface->getHighLatencyFlag();
}
+ double getAudioInputLatency() const
+ {
+ return (mAudioInterface == nullptr) ? 0 : mAudioInterface->getAudioInputLatency();
+ }
+ double getAudioOutputLatency() const
+ {
+ return (mAudioInterface == nullptr) ? 0
+ : mAudioInterface->getAudioOutputLatency();
+ }
double getLatency() const
{
return mReceiveRingBuffer == nullptr ? -1 : mReceiveRingBuffer->getLatency();
uint16_t getClientPort() { return mClientPort; }
QString getClientAddress() { return mClientAddress; }
+ double getLatency()
+ {
+ QMutexLocker lock(&mMutex);
+ return mJackTrip.isNull() ? -1 : mJackTrip->getLatency();
+ }
+
private slots:
void slotTest() { std::cout << "--- JackTripWorker TEST SLOT ---" << std::endl; }
void receivedDataUDP();
#include <iostream>
-using std::cout;
-using std::endl;
+using namespace std;
//*******************************************************************************
OscServer::OscServer(quint16 port, QObject* parent) : QObject(parent), mPort(port) {}
mOscServerSocket->readDatagram(datagram.data(), datagram.size(), &sender,
&senderPort);
- qDebug() << "Received datagram from" << sender << ":" << senderPort;
- qDebug() << " - Data:" << datagram;
+ // qDebug() << "Received datagram from" << sender << ":" << senderPort;
+ // qDebug() << " - Data:" << datagram;
#ifndef NO_OSCPP
- handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size()));
+ handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size()), sender,
+ senderPort);
#endif // NO_OSCPP
// Send a reply back to the client
// QByteArray replyData("Reply from server");
//*******************************************************************************
#ifndef NO_OSCPP
-void OscServer::handlePacket(const OSCPP::Server::Packet& packet)
+void OscServer::handlePacket(const OSCPP::Server::Packet& packet,
+ const QHostAddress& sender, quint16 senderPort)
{
try {
if (packet.isBundle()) {
// Iterate over all the packets and call handlePacket recursively.
while (!packets.atEnd()) {
- handlePacket(packets.next());
+ handlePacket(packets.next(), sender, senderPort);
}
} else {
// Convert to message
if (msg == "/config") {
const char* key = args.string();
const float value = args.float32();
- cout << "Config received - key (" << key << ") value (" << value << ")"
- << endl;
+ cout << "OSC: Config received - key (" << key << ") value (" << value
+ << ")" << endl;
if (strcmp("queueBuffer", key) == 0) {
emit signalQueueBufferChanged(static_cast<int>(value));
}
+ } else if (msg == "/get") {
+ const char* key = args.string();
+ cout << "OSC: Get request received - key (" << key << ")" << endl;
+ if (strcmp("latency", key) == 0) {
+ emit signalLatencyRequested(sender, senderPort);
+ }
} else {
// Simply print unknown messages
- cout << "Unknown message:" << msg.address() << endl;
+ cerr << "OSC: Unknown message:" << msg.address() << endl;
}
}
- } catch (std::exception& e) {
- cout << "Exception:" << e.what() << endl;
+ } catch (exception& e) {
+ cerr << "OSC: Exception:" << e.what() << endl;
}
}
#endif // NO_OSCPP
+
+void OscServer::sendLatencyResponse(const QHostAddress& sender, quint16 senderPort,
+ QVector<QString>& clientNames,
+ QVector<double>& latencies)
+{
+#ifndef NO_OSCPP
+ QByteArray datagram;
+ datagram.resize(64 * 1024);
+
+ OSCPP::Client::Packet packet(datagram.data(), 64 * 1024);
+ packet.openBundle(QDateTime::currentSecsSinceEpoch());
+ packet.openMessage("/response/latency", clientNames.size() * 2);
+ for (int i = 0; i < clientNames.size(); i++) {
+ packet.string(clientNames[i].toStdString().c_str());
+ packet.float32(latencies[i]);
+ }
+ packet.closeMessage();
+ packet.closeBundle();
+
+ datagram.resize(packet.size());
+ mOscServerSocket->writeDatagram(datagram, sender, senderPort);
+#endif // NO_OSCPP
+}
#ifndef __OSCSERVER_H__
#define __OSCSERVER_H__
+#include <QHostAddress>
#include <QObject>
+#include <QString>
#include <QUdpSocket>
+#include <QVector>
#include <QtCore>
#ifndef NO_OSCPP
virtual ~OscServer();
void start();
void stop();
+ void sendLatencyResponse(const QHostAddress& sender, quint16 senderPort,
+ QVector<QString>& clientNames, QVector<double>& latencies);
static size_t makeConfigPacket(void* buffer, size_t size, const char* key,
float value)
}
signals:
void signalQueueBufferChanged(int queueBufferSize);
+ void signalLatencyRequested(QHostAddress sender, quint16 senderPort);
private slots:
void readPendingDatagrams();
private:
void closeSocket();
#ifndef NO_OSCPP
- void handlePacket(const OSCPP::Server::Packet& packet);
+ void handlePacket(const OSCPP::Server::Packet& packet, const QHostAddress& sender,
+ quint16 senderPort);
#endif // NO_OSCPP
QSharedPointer<QUdpSocket> mOscServerSocket;
// tweak
constexpr int WindowDivisor = 8; // for faster auto tracking
constexpr double AutoHeadroomGlitchTolerance =
- 0.01; // Acceptable rate of glitches before auto headroom is increased (1.0%)
+ 0.006; // Acceptable rate of glitches before auto headroom is increased (0.6%)
constexpr double AutoHistoryWindow =
60; // rolling window of time (in seconds) over which auto tolerance roughly adjusts
constexpr double AutoSmoothingFactor =
// update headroom
if (mAutoHeadroom < 0) {
// variable headroom: automatically increase to minimize glitch counts
- // only increase headroom if doing so would have reduced the number of
- // glitches that occured over the past second by 1% or more.
- // prevent headroom from growing beyond rolling average of max.
- const int maxHeadroom = pushStat->longTermMax + 1;
int glitchesAllowed;
if (mMsecTolerance >= (mPeerFPPdurMsec * 2)) {
- // calculate glitches allowed if tolerance if above or equal to duration of
- // two packets
- glitchesAllowed =
- static_cast<int>(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP);
+ // calculate glitches allowed if tolerance is above or equal to
+ // the duration of two packets
+ glitchesAllowed = std::ceil(
+ static_cast<float>(AutoHeadroomGlitchTolerance * mSampleRate) / mPeerFPP);
} else {
// zero glitches allowed if tolerance is below duration of two packets
glitchesAllowed = 0;
// also don't require two intervals in a row (override)
mSkipAutoHeadroom = false;
}
+ // sanity check: prevent headroom from growing beyond the greater of
+ // 3x rolling average of max, or 100ms
+ const int maxHeadroom = std::max<double>(pushStat->longTermMax * 3, 100.0);
+ // only increase headroom if glitch tolerance was exceeded and doing so
+ // would have reduced the number of glitches that occured over the past second.
if (skipped > 0 && glitches > glitchesAllowed
&& mCurrentHeadroom + 1 <= maxHeadroom) {
if (mSkipAutoHeadroom) {
setDevicesWarningMsg(AudioInterface::DEVICE_WARN_BUFFER_LATENCY);
}
+ if (mDuplexMode) {
+ // duplex mode returns sum of input and output latencies
+ mAudioInputLatency = static_cast<double>(mRtAudioInput->getStreamLatency()) / 2;
+ mAudioOutputLatency = mAudioInputLatency;
+ } else {
+ mAudioInputLatency = mRtAudioInput->getStreamLatency();
+ mAudioOutputLatency = mRtAudioOutput->getStreamLatency();
+ }
+
// Setup parent class
// This MUST be after buffer size is finalized, so that plugins
// are initialized with the correct settings
return (-1);
}
- AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
- AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
-
return 0;
}
}
}
+void UdpHubListener::handleLatencyRequest(const QHostAddress& sender, quint16 senderPort)
+{
+ QVector<QString> clientNames;
+ QVector<double> latencies;
+ getClientLatencies(clientNames, latencies);
+ if (mOscServer != nullptr) {
+ mOscServer->sendLatencyResponse(sender, senderPort, clientNames, latencies);
+ }
+}
+
+void UdpHubListener::getClientLatencies(QVector<QString>& clientNames,
+ QVector<double>& latencies)
+{
+ QMutexLocker lock(&mMutex);
+ for (int i = 0; i < gMaxThreads; i++) {
+ if (mJTWorkers->at(i) != nullptr) {
+ clientNames.append(mJTWorkers->at(i)->getAssignedClientName());
+ latencies.append(mJTWorkers->at(i)->getLatency());
+ }
+ }
+}
+
//*******************************************************************************
// Returns 0 on error
int UdpHubListener::readClientUdpPort(QSslSocket* clientConnection, QString& clientName)
#include <QHostAddress>
#include <QMutex>
+#include <QString>
#include <QThread>
#include <QThreadPool>
#include <QUdpSocket>
+#include <QVector>
#include <fstream>
#include <iostream>
#include <stdexcept>
#endif
int releaseThread(int id);
void releaseDuplicateThreads(JackTripWorker* worker, uint16_t actual_peer_port);
+ void getClientLatencies(QVector<QString>& clientNames, QVector<double>& latencies);
void setConnectDefaultAudioPorts(bool connectDefaultAudioPorts)
{
void receivedNewConnection();
void stopCheck();
void queueBufferChanged(int queueBufferSize);
+ void handleLatencyRequest(const QHostAddress& sender, quint16 senderPort);
signals:
void signalStarted();
QObject::connect(mOscServer, &OscServer::signalQueueBufferChanged, this,
&UdpHubListener::queueBufferChanged, Qt::QueuedConnection);
+ QObject::connect(mOscServer, &OscServer::signalLatencyRequested, this,
+ &UdpHubListener::handleLatencyRequest, Qt::QueuedConnection);
};
/**
#include "RtAudio.h"
#endif
+#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
+#define QCHECKBOX_STATE_CHANGED QCheckBox::stateChanged
+#else
+#define QCHECKBOX_STATE_CHANGED QCheckBox::checkStateChanged
+#endif
+
#include "../Compressor.h"
#include "../CompressorPresets.h"
#include "../Limiter.h"
m_ui->patchServerCheckBox->setEnabled(false);
}
});
- connect(m_ui->authCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->authCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->usernameLabel->setEnabled(m_ui->authCheckBox->isChecked());
m_ui->usernameEdit->setEnabled(m_ui->authCheckBox->isChecked());
m_ui->passwordLabel->setEnabled(m_ui->authCheckBox->isChecked());
m_ui->passwordEdit->setEnabled(m_ui->authCheckBox->isChecked());
credentialsChanged();
});
- connect(m_ui->requireAuthCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->requireAuthCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->certLabel->setEnabled(m_ui->requireAuthCheckBox->isChecked());
m_ui->certEdit->setEnabled(m_ui->requireAuthCheckBox->isChecked());
m_ui->certBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked());
m_ui->credsBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked());
authFilesChanged();
});
- connect(m_ui->ioStatsCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->ioStatsCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->ioStatsLabel->setEnabled(m_ui->ioStatsCheckBox->isChecked());
m_ui->ioStatsSpinBox->setEnabled(m_ui->ioStatsCheckBox->isChecked());
if (!m_ui->ioStatsCheckBox->isChecked()) {
m_statsDialog->hide();
}
});
- connect(m_ui->verboseCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->verboseCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
gVerboseFlag = m_ui->verboseCheckBox->isChecked();
if (!gVerboseFlag) {
m_debugDialog->hide();
m_debugDialog->clearOutput();
}
});
- connect(m_ui->jitterCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->jitterCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->broadcastCheckBox->setEnabled(m_ui->jitterCheckBox->isChecked());
m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
&& m_ui->broadcastCheckBox->isChecked());
m_autoQueueIndicator.setText(QStringLiteral("Auto queue: disabled"));
}
});
- connect(m_ui->broadcastCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->broadcastCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
&& m_ui->broadcastCheckBox->isChecked());
m_ui->broadcastQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
&& m_ui->broadcastCheckBox->isChecked());
});
- connect(m_ui->autoQueueCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->autoQueueCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->autoQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
&& m_ui->autoQueueCheckBox->isChecked());
m_ui->autoQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
}
});
- connect(m_ui->inFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->inFreeverbCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->inFreeverbLabel->setEnabled(m_ui->inFreeverbCheckBox->isChecked());
m_ui->inFreeverbWetnessSlider->setEnabled(m_ui->inFreeverbCheckBox->isChecked());
});
- connect(m_ui->inZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->inZitarevCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->inZitarevLabel->setEnabled(m_ui->inZitarevCheckBox->isChecked());
m_ui->inZitarevWetnessSlider->setEnabled(m_ui->inZitarevCheckBox->isChecked());
});
- connect(m_ui->outFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->outFreeverbCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->outFreeverbLabel->setEnabled(m_ui->outFreeverbCheckBox->isChecked());
m_ui->outFreeverbWetnessSlider->setEnabled(
m_ui->outFreeverbCheckBox->isChecked());
});
- connect(m_ui->outZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->outZitarevCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->outZitarevLabel->setEnabled(m_ui->outZitarevCheckBox->isChecked());
m_ui->outZitarevWetnessSlider->setEnabled(m_ui->outZitarevCheckBox->isChecked());
});
- connect(m_ui->outLimiterCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->outLimiterCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->outLimiterLabel->setEnabled(m_ui->outLimiterCheckBox->isChecked());
m_ui->outClientsSpinBox->setEnabled(m_ui->outLimiterCheckBox->isChecked());
});
- connect(m_ui->connectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->connectScriptCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->connectScriptEdit->setEnabled(m_ui->connectScriptCheckBox->isChecked());
m_ui->connectScriptBrowse->setEnabled(m_ui->connectScriptCheckBox->isChecked());
});
- connect(m_ui->disconnectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() {
+ connect(m_ui->disconnectScriptCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_ui->disconnectScriptEdit->setEnabled(
m_ui->disconnectScriptCheckBox->isChecked());
m_ui->disconnectScriptBrowse->setEnabled(
"will automatically be re-enabled.)");
msgBox.setWindowTitle(QStringLiteral("JACK Not Available"));
msgBox.setCheckBox(dontBugMe);
- QObject::connect(dontBugMe, &QCheckBox::stateChanged, this, [=]() {
+ QObject::connect(dontBugMe, &QCHECKBOX_STATE_CHANGED, this, [=]() {
m_hideWarning = dontBugMe->isChecked();
});
msgBox.exec();
#include "jacktrip_types.h"
-constexpr const char* const gVersion = "2.5.1"; ///< JackTrip version
+constexpr const char* const gVersion = "2.6.0"; ///< JackTrip version
//*******************************************************************************
/// \name Default Values
delegate: ItemDelegate {
required property var modelData
required property int index
- width: parent.width
contentItem: Text {
text: modelData.label
}
property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+ property bool autoQueueBuffer: virtualstudio.queueBuffer == 0
+
function getQueueBufferString () {
- if (virtualstudio.queueBuffer == 0) {
+ let queueBuffer = virtualstudio.queueBuffer;
+ if (useStudioQueueBuffer.checkState == Qt.Checked) {
+ queueBuffer = virtualstudio.currentStudio.queueBuffer;
+ }
+ if (queueBuffer == 0) {
return "auto";
}
- return virtualstudio.queueBuffer + " ms";
+ return queueBuffer + " ms";
}
MouseArea {
CheckBox {
id: useStudioQueueBuffer
checked: virtualstudio.useStudioQueueBuffer
- text: qsTr("Use Studio settings")
+ text: qsTr("Use Studio settings (recommended)")
anchors.top: latencyDivider.bottom
anchors.topMargin: 16 * virtualstudio.uiScale
x: 168 * virtualstudio.uiScale;
}
Text {
- id: currentLatency
+ id: queueBufferText
anchors.top: latencyDivider.bottom
anchors.topMargin: 16 * virtualstudio.uiScale
anchors.right: parent.right
anchors.rightMargin: 24 * virtualstudio.uiScale
- text: "Buffer Latency: " + Math.round(virtualstudio.networkStats.recvLatency) + " ms"
+ text: "Audio Quality: " + getQueueBufferString()
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Text {
+ id: currentLatency
+ anchors.top: queueBufferText.bottom
+ anchors.topMargin: 6 * virtualstudio.uiScale
+ anchors.right: parent.right
+ anchors.rightMargin: 24 * virtualstudio.uiScale
+ text: "Ingress Jitter Latency: " + Math.round(virtualstudio.networkStats.clientBufferLatency) + " ms"
font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
color: textColour
}
+ Button {
+ id: queueBufferAutoButton
+ width: 60 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ anchors.top: useStudioQueueBuffer.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: useStudioQueueBuffer.left
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: queueBufferAutoButton.down ? browserButtonPressedColour : (queueBufferAutoButton.hovered ? browserButtonHoverColour : (autoQueueBuffer ? "#FF0000" : browserButtonColour))
+ }
+ onClicked: {
+ if (autoQueueBuffer) {
+ virtualstudio.queueBuffer = 5;
+ } else {
+ virtualstudio.queueBuffer = 0;
+ }
+ autoQueueBuffer = !autoQueueBuffer;
+ }
+ Text {
+ text: "Auto"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+
+ visible: useStudioQueueBuffer.checkState != Qt.Checked
+ }
+
Slider {
id: queueBufferSlider
value: virtualstudio.queueBuffer
onMoved: {
virtualstudio.queueBuffer = value;
}
- from: 0
+ from: 1
to: 250
stepSize: 1
padding: 0
- visible: useStudioQueueBuffer.checkState != Qt.Checked
+ visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked
anchors.top: useStudioQueueBuffer.bottom
anchors.topMargin: 16 * virtualstudio.uiScale
- x: queueBufferText.x + queueBufferText.width
- width: parent.width - x - (16 * virtualstudio.uiScale) - queueBufferText.width;
+ anchors.left: queueBufferAutoButton.right
+ anchors.leftMargin: 8 * virtualstudio.uiScale
+ width: parent.width - x - (16 * virtualstudio.uiScale);
background: Rectangle {
x: queueBufferSlider.leftPadding
}
Text {
- id: queueBufferText
- width: (64 * virtualstudio.uiScale)
- anchors.left: useStudioQueueBuffer.left
- anchors.verticalCenter: queueBufferSlider.verticalCenter
- text: getQueueBufferString()
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ id: lowerLatencyText
+ anchors.top: queueBufferSlider.bottom
+ anchors.topMargin: 8 * virtualstudio.uiScale
+ anchors.left: queueBufferSlider.left
+ anchors.leftMargin: 8 * virtualstudio.uiScale
+ text: "Lower Latency"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
color: textColour
- visible: useStudioQueueBuffer.checkState != Qt.Checked
+ visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked
+ }
+
+ Text {
+ id: higherQualityText
+ anchors.top: queueBufferSlider.bottom
+ anchors.topMargin: 8 * virtualstudio.uiScale
+ anchors.right: queueBufferSlider.right
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ text: "Higher Quality"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked
}
}
property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+ function hasStudioId () {
+ return typeof virtualstudio.currentStudio.id === 'string' && virtualstudio.currentStudio.id !== null && virtualstudio.currentStudio.id.length > 0
+ }
+
Loader {
id: studioWebLoader
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: deviceControlsGroup.top
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
- property string studioId: virtualstudio.currentStudio.id
-
- source: accessToken && studioId ? "Web.qml" : "WebNull.qml"
+ source: auth.isAuthenticated && hasStudioId() ? "Web.qml" : "WebNull.qml"
}
DeviceControlsGroup {
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: footer.top
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
- sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull
+ sourceComponent: virtualstudio.windowState === "create_studio" && auth.isAuthenticated ? createStudioWeb : createStudioNull
}
Component {
settings.javascriptCanPaste: true
settings.screenCaptureEnabled: true
profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
- url: `https://${virtualstudio.apiHost === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create?accessToken=${accessToken}&userId=${auth.userId}`
+ url: `https://${virtualstudio.apiHost === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create`
onContextMenuRequested: function(request) {
// this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop
onClicked: () => {
deviceWarningPopup.close();
audio.stopAudio(true);
- virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
virtualstudio.windowState = "connected";
virtualstudio.saveSettings();
virtualstudio.joinStudio();
anchors.topMargin: 16 * virtualstudio.uiScale
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
- text: "Your feedback has been recorded."
+ text: "Your feedback has been sent."
font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
horizontalAlignment: Text.AlignHCenter
color: textColour
let minRtt = virtualstudio.networkStats.minRtt;
let maxRtt = virtualstudio.networkStats.maxRtt;
let avgRtt = virtualstudio.networkStats.avgRtt;
+ let clientBufferLatency = virtualstudio.networkStats.clientBufferLatency;
let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed];
if (virtualstudio.networkOutage) {
return texts;
}
- texts[1] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms";
+ texts[1] = "<b>" + minRtt + " - " + maxRtt + " ms ping</b>, " + clientBufferLatency + " ms jitter";
let quality = "Poor";
let color = meterRed;
- if (avgRtt < 10 && maxRtt < 15) {
+ if (avgRtt < 10 && maxRtt < 15 && clientBufferLatency < 6) {
quality = "Excellent";
color = meterGreen;
- } else if (avgRtt < 20 && maxRtt < 30) {
+ } else if (avgRtt < 20 && maxRtt < 30 && clientBufferLatency < 9) {
quality = "Good";
color = meterYellow;
- } else if (avgRtt < 30 && maxRtt < 40) {
+ } else if (avgRtt < 30 && maxRtt < 40 && clientBufferLatency < 12) {
quality = "Fair";
color = statsOrange;
}
deviceWarningModal.open();
} else {
audio.stopAudio(true);
- virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
virtualstudio.windowState = "connected";
virtualstudio.saveSettings();
virtualstudio.joinStudio();
anchors.fill: parent
color: backgroundColour
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
property string studioId: virtualstudio.currentStudio.id
WebEngineView {
settings.javascriptCanPaste: true
settings.screenCaptureEnabled: true
profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
- url: `https://${virtualstudio.apiHost}/studios/${studioId}/live`
+ url: `https://${virtualstudio.apiHost}/studios/${web.studioId}/live`
// useful for debugging
// onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) {
id: web
anchors.fill: parent
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
property string studioId: virtualstudio.currentStudio.id
WebView {
id: webEngineView
anchors.fill: parent
httpUserAgent: `JackTrip/${virtualstudio.versionString}`
- url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+ url: `https://${virtualstudio.apiHost}/studios/${web.studioId}/live`
}
}
}
m_windowState = state;
// refresh studio list if navigating to browse window
// only if user id is empty (edge case for when logging in)
- if (m_windowState == "browse" && !m_userId.isEmpty()) {
+ if (m_windowState == "browse" && m_auth->isAuthenticated()) {
// schedule studio refresh instead of doing it now
// just to reduce risk of running into a deadlock
emit scheduleStudioRefresh(-1, false);
#endif
feedback.insert(QStringLiteral("osVersion"), QSysInfo::prettyProductName());
- QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName());
#ifdef RT_AUDIO
QString inputDevice =
QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString());
if (!inputDevice.isEmpty()) {
- sysInfo.append(QString(",input=%1").arg(inputDevice));
+ feedback.insert(QStringLiteral("inputDevice"), inputDevice);
}
QString outputDevice =
QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString());
if (!outputDevice.isEmpty()) {
- sysInfo.append(QString(",output=%1").arg(outputDevice));
+ feedback.insert(QStringLiteral("outputDevice"), outputDevice);
}
#endif
- sysInfo.append("]");
feedback.insert(QStringLiteral("rating"), rating);
- if (message.isEmpty()) {
- feedback.insert(QStringLiteral("message"), sysInfo);
- } else {
- feedback.insert(QStringLiteral("message"), message + " " + sysInfo);
+ if (!message.isEmpty()) {
+ feedback.insert(QStringLiteral("message"), message);
}
QString deviceIssues = "";
}
if (!deviceIssues.isEmpty()) {
feedback.insert(QStringLiteral("deviceIssues"), deviceIssues);
- message.append(" (deviceIssues=" + deviceIssues + ")");
}
QJsonDocument data = QJsonDocument(feedback);
m_api->submitServerFeedback(serverId, data.toJson());
+ std::cout << "Sent feedback: " << data.toJson().toStdString() << std::endl;
return;
}
return;
}
- // pop studioToJoin
- const QString targetId = m_studioToJoin;
- setStudioToJoin("");
-
// stop audio if already running (settings or setup windows)
m_audioConfigPtr->stopAudio(true);
// find and populate data for current studio
VsServerInfoPointer sPtr;
for (const VsServerInfoPointer& s : m_servers) {
- if (s->id() == targetId) {
+ if (s->id() == m_studioToJoin) {
sPtr = s;
break;
}
locker.unlock();
if (sPtr.isNull()) {
- m_failedMessage = "Unable to find studio " + targetId;
+ m_failedMessage = "Unable to find studio " + m_studioToJoin;
+ setStudioToJoin("");
emit failedMessageChanged();
emit failed();
return;
}
- m_currentStudio = *sPtr;
- emit currentStudioChanged();
-
if (m_windowState == "setup") {
- m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate());
+ m_audioConfigPtr->setSampleRate(sPtr->sampleRate());
m_audioConfigPtr->startAudio();
return;
}
+ setStudioToJoin("");
+ m_currentStudio = *sPtr;
+ emit currentStudioChanged();
+
// m_windowState == "connected"
connectToStudio();
}
return;
}
+ std::cout << "Reconnecting audio to " << m_currentStudio.host().toStdString() << ":"
+ << m_currentStudio.port() << std::endl;
+
// this needs to be synchronous to avoid both trying
// to use the audio interfaces at the same time
// note that connectionFinished() checks m_reconnectState
void VirtualStudio::handleWebsocketMessage(const QString& msg)
{
- QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object();
- QString serverStatus = serverState[QStringLiteral("status")].toString();
- bool serverEnabled = serverState[QStringLiteral("enabled")].toBool();
- QString serverCloudId = serverState[QStringLiteral("cloudId")].toString();
- int queueBuffer = serverState[QStringLiteral("queueBuffer")].toInt();
+ if (m_currentStudio.id() == "") {
+ return;
+ }
+
+ QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object();
+ const QString& serverHost = serverState[QStringLiteral("serverHost")].toString();
+ const QString& serverStatus = serverState[QStringLiteral("status")].toString();
+ const QString& serverCloudId = serverState[QStringLiteral("cloudId")].toString();
+ const QString& sessionId = serverState[QStringLiteral("sessionId")].toString();
+ const bool serverEnabled = serverState[QStringLiteral("enabled")].toBool();
+ const int serverPort = serverState[QStringLiteral("serverPort")].toInt();
+ const int queueBuffer = serverState[QStringLiteral("queueBuffer")].toInt();
// server notifications are also transmitted along this websocket, so ignore data if
// it contains "message"
if (!message.isEmpty()) {
return;
}
- if (m_currentStudio.id() == "") {
- return;
+
+ bool currentStudioUpdated = false;
+ bool serverHostOrPortUpdated = false;
+ if (serverHost != m_currentStudio.host()) {
+ m_currentStudio.setHost(serverHost);
+ currentStudioUpdated = true;
+ serverHostOrPortUpdated = true;
}
- m_currentStudio.setStatus(serverStatus);
- m_currentStudio.setEnabled(serverEnabled);
- m_currentStudio.setCloudId(serverCloudId);
- m_currentStudio.setQueueBuffer(queueBuffer);
- if (!m_jackTripRunning) {
- if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) {
- m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString());
- m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt());
- m_currentStudio.setSessionId(
- serverState[QStringLiteral("sessionId")].toString());
- completeConnection();
+ if (serverStatus != m_currentStudio.status()) {
+ m_currentStudio.setStatus(serverStatus);
+ currentStudioUpdated = true;
+ }
+ if (serverCloudId != m_currentStudio.cloudId()) {
+ m_currentStudio.setCloudId(serverCloudId);
+ currentStudioUpdated = true;
+ }
+ if (sessionId != m_currentStudio.sessionId()) {
+ m_currentStudio.setSessionId(sessionId);
+ currentStudioUpdated = true;
+ }
+ if (serverEnabled != m_currentStudio.enabled()) {
+ m_currentStudio.setEnabled(serverEnabled);
+ currentStudioUpdated = true;
+ }
+ if (serverPort != m_currentStudio.port()) {
+ m_currentStudio.setPort(serverPort);
+ currentStudioUpdated = true;
+ serverHostOrPortUpdated = true;
+ }
+ if (queueBuffer != m_currentStudio.queueBuffer()) {
+ m_currentStudio.setQueueBuffer(queueBuffer);
+ currentStudioUpdated = true;
+ if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) {
+ m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer());
}
- } else if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) {
- m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer());
}
- emit currentStudioChanged();
+ if (currentStudioUpdated) {
+ emit currentStudioChanged();
+ }
+
+ if (m_onConnectedScreen) {
+ if (!m_jackTripRunning && serverEnabled && serverStatus == QLatin1String("Ready")
+ && serverHost != "" && serverPort != 0) {
+ std::cout << "Connecting audio to " << serverHost.toStdString() << ":"
+ << serverPort << std::endl;
+ completeConnection();
+ }
+ }
}
void VirtualStudio::restartStudioSocket()
void VirtualStudio::refreshStudios(int index, bool signalRefresh)
{
// user id is required for retrieval of subscriptions
- if (m_userId.isEmpty()) {
+ if (!m_auth->isAuthenticated()) {
std::cerr << "Studio refresh cancelled due to empty user id" << std::endl;
return;
}
QApplication* VirtualStudio::createApplication(int& argc, char* argv[])
{
#if defined(Q_OS_WIN)
+#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
// Fix for display scaling like 125% or 150% on Windows
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
// QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
+ // Direct3D11 is still broken as of Qt 6.8.1
// QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11);
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
+#else // Qt 6.6.0 or later supports Direct3D 12, which works well
+ QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
+#endif
#endif
QQuickStyle::setStyle("Basic");
#if defined(Q_OS_MACOS) && (QT_VERSION > QT_VERSION_CHECK(6, 2, 6)) \
&& (QT_VERSION < QT_VERSION_CHECK(6, 8, 0))
// work-around for screen sharing bugs in qtwebengine 6.2.7-6.7.x
- qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-features=DesktopCaptureMacV2");
+ QString chromiumFlags("--disable-features=DesktopCaptureMacV2");
+ char* existingFlags = getenv("QTWEBENGINE_CHROMIUM_FLAGS");
+ if (existingFlags != nullptr) {
+ chromiumFlags.append(" ");
+ chromiumFlags.append(existingFlags);
+ }
+ qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.toUtf8());
#endif
// Initialize webengine
if (mode == m_inputMixMode)
return;
m_inputMixMode = mode;
+ if (m_inputMixMode == static_cast<int>(AudioInterface::MONO)) {
+ if (m_numInputChannels > 1) {
+ setNumInputChannels(1);
+ }
+ } else if (m_inputMixMode == static_cast<int>(AudioInterface::STEREO)
+ || m_inputMixMode == static_cast<int>(AudioInterface::MIXTOMONO)) {
+ if (m_numInputChannels == 1) {
+ setNumInputChannels(2);
+ }
+ }
emit inputMixModeChanged(mode);
return;
}
std::cout << " or: " << AudioBufferSizeInBytes << " bytes"
<< std::endl;
std::cout << gPrintSeparator << std::endl;
+ std::cout << "The Audio Input Latency is : " << ifPtr->getAudioInputLatency()
+ << std::endl;
+ std::cout << "The Audio Output Latency is: " << ifPtr->getAudioOutputLatency()
+ << std::endl;
+ std::cout << gPrintSeparator << std::endl;
std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels()
<< std::endl;
std::cout << gPrintSeparator << std::endl;
inputChannelsComboModel.push_back(element);
}
for (int i = 0; i < numDevicesChannelsAvailable; i++) {
- if (i % 2 == 0) {
- QJsonObject element = QJsonObject();
- element.insert(
- QString::fromStdString("label"),
- QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
- element.insert(QString::fromStdString("baseChannel"),
- QVariant(i).toInt());
- element.insert(QString::fromStdString("numChannels"),
- QVariant(2).toInt());
- inputChannelsComboModel.push_back(element);
- }
+ QJsonObject element = QJsonObject();
+ element.insert(
+ QString::fromStdString("label"),
+ QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+ element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt());
+ element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt());
+ inputChannelsComboModel.push_back(element);
}
m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
m_parentPtr->setBaseInputChannel(0);
m_parentPtr->setNumInputChannels(2);
}
- if (getNumInputChannels() != 1) {
- // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if
- // we're using 2 channels
- QJsonObject inputMixModeComboElement1 = QJsonObject();
- inputMixModeComboElement1.insert(QString::fromStdString("label"),
- QString::fromStdString("Stereo"));
- inputMixModeComboElement1.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::STEREO));
- QJsonObject inputMixModeComboElement2 = QJsonObject();
- inputMixModeComboElement2.insert(QString::fromStdString("label"),
- QString::fromStdString("Mix to Mono"));
- inputMixModeComboElement2.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MIXTOMONO));
- QJsonArray inputMixModeComboModel;
- inputMixModeComboModel.push_back(inputMixModeComboElement1);
- inputMixModeComboModel.push_back(inputMixModeComboElement2);
- m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
-
- // if m_inputMixMode is an invalid value, set it to "stereo" by default
- // given that we are using 2 channels
+
+ // include all options in the mix mode combo
+ QJsonObject inputMixModeComboElement0 = QJsonObject();
+ inputMixModeComboElement0.insert(QString::fromStdString("label"),
+ QString::fromStdString("Mono"));
+ inputMixModeComboElement0.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MONO));
+ QJsonObject inputMixModeComboElement1 = QJsonObject();
+ inputMixModeComboElement1.insert(QString::fromStdString("label"),
+ QString::fromStdString("Stereo"));
+ inputMixModeComboElement1.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::STEREO));
+ QJsonObject inputMixModeComboElement2 = QJsonObject();
+ inputMixModeComboElement2.insert(QString::fromStdString("label"),
+ QString::fromStdString("Mix to Mono"));
+ inputMixModeComboElement2.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MIXTOMONO));
+ QJsonArray inputMixModeComboModel;
+ inputMixModeComboModel.push_back(inputMixModeComboElement0);
+ inputMixModeComboModel.push_back(inputMixModeComboElement1);
+ inputMixModeComboModel.push_back(inputMixModeComboElement2);
+ m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+ if (m_parentPtr->getNumInputChannels() == 2) {
+ // Set the input mix mode to "Stereo" if we're using 2 channels
if (getInputMixMode() != static_cast<int>(AudioInterface::STEREO)
&& getInputMixMode() != static_cast<int>(AudioInterface::MIXTOMONO)) {
m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::STEREO));
} else {
// Set the input mix mode to just have "Mono" as the option if we're using 1
// channel
- QJsonObject inputMixModeComboElement = QJsonObject();
- inputMixModeComboElement.insert(QString::fromStdString("label"),
- QString::fromStdString("Mono"));
- inputMixModeComboElement.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MONO));
- QJsonArray inputMixModeComboModel;
- inputMixModeComboModel.push_back(inputMixModeComboElement);
- m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
-
- // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO
if (getInputMixMode() != static_cast<int>(AudioInterface::MONO)) {
m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
}
// selected device
QJsonArray outputChannelsComboModel;
for (int i = 0; i < numDevicesChannelsAvailable; i++) {
- if (i % 2 == 0) {
- QJsonObject element = QJsonObject();
- element.insert(
- QString::fromStdString("label"),
- QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
- element.insert(QString::fromStdString("baseChannel"),
- QVariant(i).toInt());
- element.insert(QString::fromStdString("numChannels"),
- QVariant(2).toInt());
- outputChannelsComboModel.push_back(element);
- }
+ QJsonObject element = QJsonObject();
+ element.insert(
+ QString::fromStdString("label"),
+ QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+ element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt());
+ element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt());
+ outputChannelsComboModel.push_back(element);
}
m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
json.insert(QLatin1String("high_latency"),
m_audioConfigPtr->getHighLatencyFlag());
json.insert(QLatin1String("network_outage"), m_networkOutage);
- json.insert(QLatin1String("recv_latency"),
- m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency());
+ json.insert(QLatin1String("audio_input_latency"),
+ m_jackTrip.isNull()
+ ? 0
+ : (qint64)(m_jackTrip->getAudioInputLatency() * 10000));
+ json.insert(QLatin1String("audio_output_latency"),
+ m_jackTrip.isNull()
+ ? 0
+ : (qint64)(m_jackTrip->getAudioOutputLatency() * 10000));
+ json.insert(
+ QLatin1String("client_buffer_latency"),
+ m_jackTrip.isNull() ? 0 : (qint64)(m_jackTrip->getLatency() * ns_per_ms));
// For the internal application UI, ms will suffice. No conversion needed
QJsonObject pingStats = {};
((int)(10 * stats.stdDevRtt)) / 10.0);
pingStats.insert(QLatin1String("highLatency"),
m_audioConfigPtr->getHighLatencyFlag());
- pingStats.insert(QLatin1String("recvLatency"),
- m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency());
+ pingStats.insert(QLatin1String("audioInputLatency"),
+ m_jackTrip.isNull() ? 0 : m_jackTrip->getAudioInputLatency());
+ pingStats.insert(QLatin1String("audioOutputLatency"),
+ m_jackTrip.isNull() ? 0 : m_jackTrip->getAudioOutputLatency());
+ pingStats.insert(
+ QLatin1String("clientBufferLatency"),
+ m_jackTrip.isNull() ? 0 : ((int)(10 * m_jackTrip->getLatency())) / 10.0);
emit updateNetworkStats(pingStats);
}